﻿// (c)2010 David Schwarz (david.schwarz@live.com)
// This file is licensed under GNU GPL v2 license, see more at http://www.gnu.org/licenses/gpl-2.0.html
namespace Z2.Core
{

	#region Using

	using System;
	using System.Collections.Generic;
	using System.Diagnostics;
	using System.IO;
	using System.Xml;
	using Microsoft.Xna.Framework;
	using Microsoft.Xna.Framework.Graphics;
	using Z2.Language;
	using Z2.Common;

	#endregion

	/// <summary>
	/// Třída pro mapu
	/// </summary>
	public class World
	{

		#region Consts

		internal const string WORLD_DIR = "Maps";
		private const string DEFINITION_DIR = @"Objects\{0}.xml";
		
		#endregion

		#region Members

		private static bool _isLoading;
		private static World _currentWorld;
		private static object _syncObjects = new object();
		private static Dictionary<string, FieldDefinition> _definitions;

		private string m_fileName;
		private string m_name;
		private string m_author;

		private Rectangle m_bounds;
		private Rectangle m_sectorBounds;

		private WorldField[,] m_field;
		private WorldField[,] m_fieldF;

		private WorldSector[,] m_sectors;

		private List<WorldField> m_foreground;
		private List<ObjectBase> m_objects;
		private List<EffectBase> m_effects;
		private Vector2 m_playerStart;

		private Player m_player;

		#endregion

		#region Constructor

		/// <summary>
		/// Statický konstruktor
		/// </summary>
		static World()
		{
			_definitions = new Dictionary<string, FieldDefinition>();
			_isLoading = true;
		}

		/// <summary>
		/// Konstruktor
		/// </summary>
		/// <param name="fileName">Název souboru mapy (bez přípony)</param>
		public World(string fileName)
		{
			this.m_fileName = fileName;
		}

		#endregion

		#region Properties

		/// <summary>
		/// Vrací název mapy
		/// </summary>
		public string Name
		{
			get
			{
				return this.m_name;
			}
		}

		/// <summary>
		/// Vrací jméno autora
		/// </summary>
		public string Author
		{
			get
			{
				return this.m_author;
			}
		}

		/// <summary>
		/// Vrací seznam použitých efektů v tomto levelu
		/// </summary>
		public List<EffectBase> Effects
		{
			get
			{
				return this.m_effects;
			}
		}

		/// <summary>
		/// Vrací seznam všech objektů na mapě
		/// </summary>
		//public List<ObjectBase> Objects
		//{
		//    get
		//    {
		//        return this.m_objects;
		//    }
		//}

		/// <summary>
		/// Vrací hranice mapy
		/// </summary>
		public Rectangle Bounds
		{
			get
			{
				return this.m_bounds;
			}
		}

		public static bool IsLoading
		{
			get
			{
				return _isLoading;
			}
		}

		/// <summary>
		/// Vrací aktuální mapu
		/// </summary>
		public static World Current
		{
			get
			{
				return _currentWorld;
			}
		}

		/// <summary>
		/// Hráč
		/// </summary>
		public Player Player
		{
			get
			{
				return this.m_player;
			}
		}

		#endregion

		#region Methods

		/// <summary>
		/// Metoda uvolní paměť mapy
		/// </summary>
		public void Release()
		{
			SpriteManager.Instance.ReleaseGroup(World.WORLD_DIR);

			if (this.m_sectors != null && this.m_sectors.Length > 0)
			{
				GC.SuppressFinalize(this.m_sectors);
			}

			if (this.m_field != null && this.m_field.Length > 0)
			{
				GC.SuppressFinalize(this.m_field);
			}

			if (this.m_fieldF != null && this.m_fieldF.Length > 0)
			{
				GC.SuppressFinalize(this.m_fieldF);
			}

			GC.WaitForPendingFinalizers();

			this.m_field = null;
			this.m_fieldF = null;
			this.m_sectors = null;

			if (this.m_objects == null)
			{
				this.m_objects = new List<ObjectBase>();
			}
			else this.m_objects.Clear();

			if (this.m_effects == null)
			{
				this.m_effects = new List<EffectBase>();
			}
			else this.m_effects.Clear();

			if (this.m_foreground == null)
			{
				this.m_foreground = new List<WorldField>();
			}
			else this.m_foreground.Clear();
		}

		public List<ObjectBase> GetSectorsObjects(ObjectBase objectBase)
		{
			List<WorldSector> sectors = new List<WorldSector>();

			// levý horní
			int x = ((int)objectBase.Position.X - WorldField.SIZE) / WorldSector.SIZE;
			int y = ((int)objectBase.Position.Y - WorldField.SIZE) / WorldSector.SIZE;

			if(this.m_sectors[x, y] != null)
				sectors.Add(this.m_sectors[x, y]);

			// pravý horní
			x = ((int)objectBase.Position.X + objectBase.Sprite.FrameWidth + WorldField.SIZE) / WorldSector.SIZE;
			y = ((int)objectBase.Position.Y - WorldField.SIZE) / WorldSector.SIZE;

			if (this.m_sectors[x, y] != null && !sectors.Contains(this.m_sectors[x, y]))
				sectors.Add(this.m_sectors[x, y]);

			// levý dolní
			x = ((int)objectBase.Position.X - WorldField.SIZE) / WorldSector.SIZE;
			y = ((int)objectBase.Position.Y + objectBase.Sprite.FrameHeight + WorldField.SIZE) / WorldSector.SIZE;

			if (this.m_sectors[x, y] != null && !sectors.Contains(this.m_sectors[x, y]))
				sectors.Add(this.m_sectors[x, y]);

			// pravý dolní
			x = ((int)objectBase.Position.X + objectBase.Sprite.FrameWidth + WorldField.SIZE) / WorldSector.SIZE;
			y = ((int)objectBase.Position.Y + objectBase.Sprite.FrameHeight + -WorldField.SIZE) / WorldSector.SIZE;

			if (this.m_sectors[x, y] != null && !sectors.Contains(this.m_sectors[x, y]))
				sectors.Add(this.m_sectors[x, y]);

			List<ObjectBase> objs = new List<ObjectBase>();
			foreach (WorldSector sector in sectors)
				objs.AddRange(sector.Objects.ToArray());

			return objs;
		}

		/// <summary>
		/// Metoda zaregistruje objekt <paramref name="objectBase"/> do konkrétního sektoru
		/// </summary>
		/// <param name="objectBase"><see cref="ObjectBase"/></param>
		public void RegisterToSector(ObjectBase objectBase)
		{
			int x = ((int)objectBase.Position.X + objectBase.Sprite.FrameWidth / 2) / WorldSector.SIZE;
			int y = ((int)objectBase.Position.Y + objectBase.Sprite.FrameHeight / 2) / WorldSector.SIZE;
			if (x < 0 || x >= this.m_sectorBounds.Width || y < 0 || y >= this.m_sectorBounds.Height)
			{
				return;
			}
			if (this.m_sectors[x, y] == null)
			{
				this.m_sectors[x, y] = new WorldSector();
			}

			this.m_sectors[x, y].Register(objectBase);

			//if (objectBase is Player)
			//{
			//    Debug.WriteLine(string.Format("{0};{1}", x, y));
			//}
		}

		/// <summary>
		/// Metoda zaregistruje objekt do mapy a sektoru
		/// </summary>
		/// <param name="objectBase"><see cref="ObjectBase"/> nebo potomek</param>
		public void RegisterObject(ObjectBase objectBase)
		{
			lock (_syncObjects)
			{
				if (objectBase is Player)
				{
					this.m_objects.Insert(this.m_objects.Count, objectBase);
					objectBase.Position = this.m_playerStart;
					this.m_player = (Player)objectBase;
				}
				else
				{
					this.m_objects.Add(objectBase);
				}
			}
		}

		/// <summary>
		/// Metoda načte definici z XML a uloží ji do cache.
		/// </summary>
		/// <param name="name">Název definice</param>
		/// <returns><see cref="FieldDefinition"/></returns>
		private static FieldDefinition LoadDefinition(string name)
		{
			if (_definitions.ContainsKey(name))
			{
				return _definitions[name];
			}

			string fileName = string.Format(World.DEFINITION_DIR, name);
			if (!File.Exists(fileName))
			{
				throw new FileNotFoundException(Strings.DefinitionFileNotFound, fileName);
			}

			XmlDocument document = new XmlDocument();
			document.Load(fileName);
			FieldDefinition definition = new FieldDefinition(document.ChildNodes[1]);
			
			_definitions.Add(name, definition);

			return definition;
		}

		/// <summary>
		/// Metoda načte pole mapy podle definice
		/// </summary>
		/// <param name="name">Název definice</param>
		/// <returns><see cref="WorldField"/></returns>
		private WorldField LoadFieldByDefinition(string name)
		{
			FieldDefinition definition = World.LoadDefinition(name);
			return new WorldField(definition);
		}

		/// <summary>
		/// Metoda načte objekt podle jeho definice
		/// </summary>
		/// <param name="name">Název definice objektu</param>
		/// <returns><see cref="ObjectBase"/></returns>
		private ObjectBase LoadObjectByDefintion(string name)
		{
			FieldDefinition definition = World.LoadDefinition(name);
			ObjectBase objectBase = null;

			switch (name.ToLower().Trim())
			{
				case WorldObjects.CRATE:
					objectBase = new Crate();
					break;
				default:
					throw new NotSupportedException(string.Format(Strings.ObjectNotSupported, name));
			}

			if (objectBase != null)
			{
				// TODO
			}

			return objectBase;
		}

		/// <summary>
		/// Metoda vloží do mapy políčko <paramref name="field"/>
		/// </summary>
		/// <param name="draw">Určuje, kde se má dané políčko vykreslovat</param>
		/// <param name="field">Políčko mapy</param>
		/// <param name="x">X</param>
		/// <param name="y">Y</param>
		private void Put(WorldFieldDraw draw, WorldField field, int x, int y)
		{
			switch (draw)
			{
				case WorldFieldDraw.Background:
					this.m_field[x, y] = field;
					return;
				case WorldFieldDraw.Foreground:
					this.m_fieldF[x, y] = field;
					return;
				default:
					throw new NotSupportedException();
			}
		}

		/// <summary>
		/// Metoda načte data mapy
		/// </summary>
		/// <remarks>
		/// Upravit načítání objektů a políček tak, aby se zbytečně neduplikoval kód načítání
		/// společných atributů.
		/// </remarks>
		public void Load()
		{
			_currentWorld = this; // Uložení ref. na akt. mapu
			_isLoading = true;
			Stopwatch watch = new Stopwatch();
			watch.Start();
			// Nejdřív uvolníme zabranou paměť
			this.Release();

			string fileName = World.GetFullPath(this.m_fileName);
			if (!File.Exists(fileName))
			{
				throw new FileNotFoundException(Strings.MapFileNotFound, fileName);
			}

			XmlDocument document = new XmlDocument();
			document.Load(fileName);

			// Nejdříve načteme hranice
			XmlNode mapNode = document.GetElementsByTagName(XmlConst.ELM_MAP)[0];
			this.m_bounds = new Rectangle();
			this.m_bounds.Width = Convert.ToInt32(mapNode.Attributes[XmlConst.ATT_BOUNDW].Value);
			this.m_bounds.Height = Convert.ToInt32(mapNode.Attributes[XmlConst.ATT_BOUNDH].Value);
			this.m_bounds.X = Convert.ToInt32(mapNode.Attributes[XmlConst.ATT_BOUNDX].Value);
			this.m_bounds.Y = Convert.ToInt32(mapNode.Attributes[XmlConst.ATT_BOUNDY].Value);

			// Alokujeme potřebnou paměť
			this.m_field = new WorldField[this.m_bounds.Width, this.m_bounds.Height]; // Prostředek
			this.m_fieldF = new WorldField[this.m_bounds.Width, this.m_bounds.Height]; // Popředí

			this.m_sectorBounds = new Rectangle(0, 0, 
				((this.m_bounds.Width * WorldField.SIZE) / WorldSector.SIZE) + 1,
				((this.m_bounds.Height * WorldField.SIZE) / WorldSector.SIZE) + 1);
			this.m_sectors = new WorldSector[this.m_sectorBounds.Width, this.m_sectorBounds.Height]; // Inicializace paměti pro sektory

			this.m_playerStart = new Vector2( // Startovní pozice hráče
				Convert.ToSingle(mapNode.Attributes[XmlConst.ATT_STARTX].Value) * WorldField.SIZE,
				Convert.ToSingle(mapNode.Attributes[XmlConst.ATT_STARTY].Value) * WorldField.SIZE);

			// Zpracujeme prostředí
			string definition = string.Empty;
			int x, y;

			//
			// Načteme pole mapy
			//
			XmlNodeList fieldList = document.GetElementsByTagName(XmlConst.ELM_FIELD);
			foreach (XmlNode fieldNode in fieldList)
			{
				definition = fieldNode.Attributes[XmlConst.ATT_DEFINITION].Value;
				x = Convert.ToInt32(fieldNode.Attributes[XmlConst.ATT_X].Value);
				y = Convert.ToInt32(fieldNode.Attributes[XmlConst.ATT_Y].Value);

				FieldDefinition def = World.LoadDefinition(definition);
				WorldField field = this.LoadFieldByDefinition(definition);
				field.Position = new Vector2(x * WorldField.SIZE, y * WorldField.SIZE);
				this.Put(def.Draw, field, x, y);
			}

			//
			// Načteme objekty
			//
			XmlNodeList objectList = document.GetElementsByTagName(XmlConst.ELM_OBJECT);
			foreach (XmlNode objectNode in objectList)
			{
				ObjectBehaviorBase behavior = null;
				definition = objectNode.Attributes[XmlConst.ATT_DEFINITION].Value;
				x = Convert.ToInt32(objectNode.Attributes[XmlConst.ATT_X].Value);
				y = Convert.ToInt32(objectNode.Attributes[XmlConst.ATT_Y].Value);

				ObjectBase mapObject = this.LoadObjectByDefintion(definition);
				mapObject.Position = new Vector2(x * WorldField.SIZE, y * WorldField.SIZE);
				mapObject.InitialPosition = new Vector2(x * WorldField.SIZE, y * WorldField.SIZE);
				mapObject.Type = (ObjectType)Convert.ToInt32(objectNode.Attributes[XmlConst.ATT_TYPE].Value);

				if(mapObject.Type == ObjectType.Moveable)
				{
					behavior = new GravityBehavior();
				}

				if (mapObject.Type == ObjectType.Bendable)
				{
					behavior = new BendableBehavior();
					((BendableBehavior)behavior).Bend = Convert.ToInt32(objectNode.Attributes[XmlConst.ATT_BEND].Value);
				}

				mapObject.Behavior = behavior;

				this.RegisterObject(mapObject);
			}

			_isLoading = false;
			watch.Stop();
			Debug.WriteLine(string.Format("Map {0} loaded. It took {1}ms.", 
				string.IsNullOrEmpty(this.m_name)? "unnamed" : this.m_name, 
				watch.ElapsedMilliseconds));
		}

		/// <summary>
		/// Vykreslení aktuální obrazovky mapy
		/// </summary>
		/// <param name="batch"><see cref="SpriteBatch"/></param>
		public void Draw(SpriteBatch batch)
		{
			// Získáme kopii aktuální pozice (tohle je celkem důležité,
			// jinak by docházeli k "trhání" obrazu).
			Camera camera = Camera.Instance;
			
			WorldField field = null;

			camera.Lock();
			// Vykreslujeme jen to, co je vidět (hranice získáme z kamery)
			for (int i = camera.Screen.X; i < camera.Screen.Width; ++i)
			for (int j = camera.Screen.Y; j < camera.Screen.Height; ++j)
			{
				field = this.m_field[i, j];
				if (field == null)
				{
					continue;
				}

				field.Draw(batch, Vector2.Add(field.Position, camera.Position));
			}

			foreach (ObjectBase objBase in this.m_objects)
			{
				objBase.Draw(batch);
			}

			for (int i = camera.Screen.X; i < camera.Screen.Width; ++i)
			for (int j = camera.Screen.Y; j < camera.Screen.Height; ++j)
			{
				field = this.m_fieldF[i, j];
				if (field == null)
				{
					continue;
				}

				field.Draw(batch, Vector2.Add(field.Position, camera.Position));
			}
			
			camera.Unlock();
		}

		/// <summary>
		/// Metoda získá celou cestu k souboru mapy
		/// </summary>
		/// <param name="fileName">Název souboru mapy, bez přípony.</param>
		/// <returns>Celá cesta</returns>
		private static string GetFullPath(string fileName)
		{
			return string.Format("{0}/{1}.zmap", World.WORLD_DIR, fileName);
		}

		/// <summary>
		/// Metoda provede otestování kolize objektu <paramref name="objectBase"/> za pomoci kolizního čtverce <paramref name="objectRect"/> s<br/>
		/// touto mapou.<br/>
		/// </summary>
		/// <param name="objectBase">Objekt, pro který se testuje kolize</param>
		/// <param name="objectRect">Kolizní čtverec objektu</param>
		/// <returns>V případě, že je nalezena kolize, vrací se instance <see cref="InterectionResult"/>, jinak se vrací <see cref="InteresectionResult.None"/>.</returns>
		internal IntersectionResult Intersects(ObjectBase objectBase, Rectangle objectRect)
		{
			// Vytvoříme si hranice, ze kterých budeme přebírat jednotky mapy k testování
			int left = ((int)objectBase.Position.X) / WorldField.SIZE;
			int top = ((int)objectBase.Position.Y) / WorldField.SIZE;
			int right = left + ((int)objectBase.Sprite.FrameWidth) / WorldField.SIZE;
			int bottom = top + ((int)objectBase.Sprite.FrameHeight) / WorldField.SIZE;

			// Rozšíříme hranice
			--top;
			--left;
			++right;
			++bottom;

			// Otestování hranic
			for(int i = left; i <= right; ++i)
			for (int j = top; j <= bottom; ++j)
			{
				// Mimo hranice mapy nic testovat nechceme
				if (i < this.m_bounds.X || j < this.m_bounds.Y || j >= this.m_bounds.Height || i >= this.m_bounds.Width)
				{
					continue;
				}

				WorldField field = this.m_field[i, j]; // Získáme jednotku mapy
				if (field == null)
				{
					continue;
				}

				if (field.IsSolid) // Pokud je pole označeno jako neprůchozí
				{
					// Vytvoříme kolizní čtverec pole
					// (TODO: Možná by stálo zato pomocí make optimalizace to ukládat přímo do WorldField).
					Rectangle rect = new Rectangle(
						i * WorldField.SIZE,
						j * WorldField.SIZE,
						WorldField.SIZE,
						WorldField.SIZE);

					if (objectRect.Intersects(rect)) // Pokud spolu čtverce kolidují
					{
						// Máme kolizi
						IntersectionResult result = new IntersectionResult();
						result.IsCollision = true;
						result.Rectangle = rect;
						// Nastavíme z jakého směru kolize přišla
						result.HorizontalType = (objectRect.Center.X > rect.Center.X) ? IntersectionHorizontalType.FromRight : IntersectionHorizontalType.FromLeft;
						result.VerticalType = (objectRect.Center.Y > rect.Center.Y) ? IntersectionVerticalType.FromBottom : IntersectionVerticalType.FromTop;

						return result;
					}
				}
			}

			// Nope, žádná kolize
			return IntersectionResult.None;
		}

		#endregion

	}
}
